/*
 * Miccsnookersim, a snooker simulation with AI
 * Copyright (C) 2009 Enno Ruijters, Frans van den Heuvel, Yannick Thimister,
 * Ali B. Ozmen, Esther Verhoef, Achim Leydecker
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */



//fout: Aangeraakte bal moet ook de gepotte bal zijn. 
//      Het is mogelijk meer dan 1 rode ballen te potten. 
//      Einde spel. Nadat de roze bal is gepot, mag er geen foul meer gebeuren, anders is het spel afgelopen.



import java.awt.Color;
import java.util.Arrays;

public class GameLogic implements TableListener
{
	private boolean colourOn;
	private boolean snookerMode;
	private boolean finalMode;
	private int pointsPlayerA, pointsPlayerB;
	private boolean isPlayerAPlaying;
	private int xPos;
	private Ball firstTouchedBall;
	private Ball[] pottedBalls;
	private Player[] players;
	private Table table, tableBeforeShot;
   	private int yPos;
	private int pointsLastShot;
	
	public GameLogic(Table t, Player[] p)
	{
		colourOn = false;
		table = t;
		players = p;
		tableBeforeShot = table.clone();
		isPlayerAPlaying = true;
	}

	public GameLogic(Table t)
	{
		colourOn = false;
		table = t;
		tableBeforeShot = table.clone();
		isPlayerAPlaying = true;
	}

	public void setPlayers(Player[] players)
	{
		this.players = players;
	}
	
	private GameLogic()
	{
	}
	
	public void wasShotLegal(Ball firstTouched, Ball[] potted, Ball[] previousPos, Ball[] currentPos) 
	{
		// check if the player is snookered and update snookerMode
		isPlayerSnookered(currentPos);
		
		int[] touchPoints = new int[2];
		int pottedPoints;
		
		 // are we in final mode?
		 if(finalMode) {
		 		
		 	touchPoints = isFirstTouchLegalFinalMode(firstTouched, previousPos);
		 	pottedPoints = wasPottingLegalFinalMode(potted, currentPos, touchPoints[1]);
		 }
		 else {
		 	
		 	touchPoints[0] = isFirstTouchLegal(firstTouched);
		 	pottedPoints = wasPottingLegal(firstTouched, potted, currentPos);
			if (pottedPoints > 0)
				colourOn = !colourOn;
			if (pottedPoints < 0 || touchPoints[0] < 0)
				colourOn = false;
		 }

		 if (pottedWhiteBall() && pottedPoints > -4)
			 pottedPoints = -4;
		 		
		 	if(touchPoints[0] < 0 || pottedPoints < 0) {
		 			
		 		// the shot was illegal
		 			
		 		if(touchPoints[0] < pottedPoints) {
		 				
		 			// add touchPoints to opponent
		 			updatePoints(isPlayerAPlaying, touchPoints[0]);
		 				
		 			if(snookerMode) {
		 				
		 				// let opponent decide if shot has to be redone
		 					// if so, currentPos == previous pos
		 			}
		 			else changePlayer(isPlayerAPlaying);
		 		}
		 		else {
		 			
		 			// add pottedPoints to opponent
		 			updatePoints(isPlayerAPlaying, pottedPoints);
		 				
		 			if(snookerMode) {
		 				
		 				// let opponent decide if shot has to be redone
		 					// if so, currentPos == previousPos
		 			}
		 			else changePlayer(isPlayerAPlaying);
		 		}
		 	}
		 		
		 	else if(touchPoints[0] == 0 && pottedPoints == 0) {
		 			
		 		// legal shot, but no points scored, so change player
		 		changePlayer(isPlayerAPlaying);
		 	}
		 	
			else if(touchPoints[0] == 0 && pottedPoints > 0) {
					
				// legal shot, points are scored
				updatePoints(isPlayerAPlaying, pottedPoints);
					

			}
	}

	private boolean pottedWhiteBall()
	{
		if (pottedBalls == null)
			return false;
		for (Ball b : pottedBalls)
			if (b.getValue() == 0)
				return true;
		return false;
	}
	
	private int isFirstTouchLegal(Ball firstTouched)
	{
		// was no ball touched?
		if(firstTouched == null) {
			
			return -4;
		}
		
		// was colour on or not?
		if (colourOn){
			
			// colour was on. now check if the first touched ball was a colour
			if (firstTouched.getValue() > 1){
				
				// the first touched ball was coloured, so we have a legal first touch
				
				return 0;
			}
			// first touch was not a colour
			else {
				
				// minimum penalty is 4
				return -4;
			}
		}
		// colour was not on
		else {
			
			// red was on. check if first touched was a red ball
			if (firstTouched.getValue() == 1) {
				
				// legal first touch
				return 0;
			}
			// red was on, but a colour was first touched
			else {
				
				// coulour value = penalty, but minimum penalty is 4
				if (firstTouched.getValue() < 4) {
										
					return -4;
				}
				else {
					
					return (firstTouched.getValue() * -1);
				}
			}
		}
	}
	
	private int wasPottingLegal(Ball firstTouched, Ball[] potted, Ball[] currentPos)
	{
		// check if and how many balls were potted
		
		if (potted == null) {
						
			// no ball was potted, legal "potting"
			return 0;
		}
		else if (potted.length == 1) {
			
			// one ball was potted, check if it was "on"
			if (colourOn) {
				
				// colour was on, but was a coloured one potted?
				if (potted[0].getValue() > 1) {
	
					if(potted[0].getValue() == firstTouched.getValue()) {
					replace(potted[0], currentPos);
					return potted[0].getValue();
					}
			
					else {
					return -4;
					}
					
//					// the coloured balls are always replaced on the table
//					replace(potted[0], currentPos);
//					
//					return potted[0].getValue();
				}
				// a red ball was potted, 4 penalty points!
				else return -4;
			}	
			// one ball potted, colour not on
			else {
				
				if (potted[0].getValue() > 1) {
					 
					 replace(potted[0], currentPos);
					 
					 // coulour value = penalty, but minimum penalty is 4
					if (potted[0].getValue() < 4) {
					
						return -4;
					}
					else {
					
						return (potted[0].getValue() * -1);
					}
				}
				
				// a red one was potted, legal shot
				else return potted[0].getValue();
			}
		}
		// more than 1 ball pottet is a foul
		else {
			
			// filter the coloured balls out, if there were some coulours potted. they are replaced on the table
			Arrays.sort(potted);
			
			// returnValue is used to store the highest value of all potted colours. this is needed to calculate the penalty
			int returnValue = 0;
			
			// replace the colours, starting with the highest value
			for(int i = potted.length - 1; i >= 0; i--) {
				
				if(potted[i] != null && potted[i].getValue() > 1) {
					
					// only take the highest value
					if(potted[i].getValue() > returnValue) returnValue = potted[i].getValue();
					
					replace(potted[i], currentPos);
				}
			}
			
			if(returnValue > 4) return (returnValue * -1);
			else return -4;
		}
	}
	
	private void replace(Ball colour, Ball[] currentPos)
	{
		// put the constants of Table into an array. we need this to connect colour with the right constant
		Vector[] colourConstants = {Table.POSITION_YELLOW, Table.POSITION_GREEN, Table.POSITION_BROWN, Table.POSITION_BLUE, Table.POSITION_PINK, Table.POSITION_BLACK};
		
		// set colour back to its initial position and check if it's occupied
		assert(colour.getValue() != 0 && colour.getValue() != 1);
		colour.setPosition(colourConstants[colour.getValue() - 2]);
		table.replaceBall(colour);
		
		if(isPositionOccupied(colour, currentPos)) {
			
			// if the original spot is occupied, we try to put it on the highest spot availible
			// start with checking the black spot
			
			Ball checkingBall = new Ball(Table.POSITION_BLACK, Color.BLACK, 7);
			
			if(isPositionOccupied(checkingBall, currentPos)) {
				
				// black is occupied, check pink
				checkingBall = new Ball(Table.POSITION_PINK, Color.MAGENTA, 6);
				if(isPositionOccupied(checkingBall, currentPos)){
					
					// pink is occupied, check blue
					checkingBall = new Ball(Table.POSITION_BLUE, Color.BLUE, 5);
					if(isPositionOccupied(checkingBall, currentPos)) {
						
						//blue is occupied, check brown
						checkingBall = new Ball(Table.POSITION_BROWN, new Color(150, 10, 50), 4);
						if(isPositionOccupied(checkingBall, currentPos)) {
							
							//brown is occupied, check green
							checkingBall = new Ball(Table.POSITION_GREEN, Color.GREEN, 3);
							if(isPositionOccupied(checkingBall, currentPos)) {
								
								//green is occupied, check yellow
								checkingBall = new Ball(Table.POSITION_YELLOW, Color.YELLOW, 2);
								if(isPositionOccupied(checkingBall, currentPos)) {
									
									//yellow is occupied, we have to place the ball as close to its original spot as possible
									// for now, we just try to put it on its right side
									
									boolean isPlaced = false;
									// randomly take the radius as difference in position
									double deltaX = Ball.RADIUS;
									
									// go further right until the ball has been placed
									while(!isPlaced) {
										
										// place colour a little bit to the rigth
										double[] newCoordinates = colour.getPosition().getElements();
										newCoordinates[0] += deltaX;
										
										colour.setPosition(new Vector(newCoordinates[0], newCoordinates[1], newCoordinates[2]));
										
										if(!isPositionOccupied(colour, currentPos)){
											
											// we found a free position, exit while loop
											isPlaced = true;
										}
									}
								}
								// yellow not occupied, place it there
								else colour.setPosition(Table.POSITION_YELLOW);
							}
							// green not occupied, place it there
							else colour.setPosition(Table.POSITION_GREEN);
						}
						// brown not occupied, place it there
						else colour.setPosition(Table.POSITION_BROWN);
					}
					// blue not occupied, place it there
					else colour.setPosition(Table.POSITION_BLUE);
				}
				// pink not occupied, place it there
				else colour.setPosition(Table.POSITION_PINK);
			}
			// black not occupied, place it there
			else colour.setPosition(Table.POSITION_BLACK);
		}	
	}
	
	private boolean isPositionOccupied(Ball ball, Ball[] balls)
	{
		for(int i = 0; i < balls.length; i++){
			
			// check if one of the balls of balls collides with ball
			if(ball != balls[i] &&
			   Physics.areColliding(ball, balls[i])){
				return true;
			}
		}
		
		// no collisions were detected
		return false;
	}
	
	private boolean isPlayerSnookered(Ball[] currentPos)
	{
		// first find the cue ball
		Ball cueBall = null;
		for(Ball ball : currentPos) {
			
			if(ball.getValue() == 0) cueBall = ball;
		}
		if (cueBall == null)
			return false; /* The player can now return the ball,
					 so why place it to snooker yourself? */

		if (finalMode) {
			if (currentPos.length == 1)
				return false;
			Arrays.sort(currentPos);
			/* Ball 1 so we skip the cue ball */
			if (table.canHit(cueBall, currentPos[1])) {
				this.snookerMode = false;
				return false;
			} else {
				this.snookerMode = true;
				return true;
			}
		}

		
		// check if the cue ball can hit any "on-ball" directly
		// start with checking the coloured balls if colour is on
		if(colourOn) {
			
			// test all coloured balls
			for(Ball ball : currentPos) {
				
				if (ball.getValue() > 1) {
					
					if (table.canHit(cueBall, ball)){
						
						this.snookerMode = false;
						 return false;
					}
				}
			}
			
			// no ball can be hit directly
			this.snookerMode = true;
			return true;
		}
		else {
			
			for(Ball ball : currentPos) {
				
				// only check red balls
				if(ball.getValue() == 1) {
					
					if(table.canHit(cueBall, ball)) {
						
						this.snookerMode = false;
						return false;
					}
				}
			}
			
			// found no possible direct hit
			this.snookerMode = true;
			return true;
		} 
	}

	public boolean isSnookered()
	{
		return isPlayerSnookered(table.getBalls(false));
	}
	
	public boolean isFinalMode(Ball[] currentPos)
	{
		// check if there are any red balls left on the table
		for(Ball ball : currentPos) {
			
			if(ball.getValue() == 1
			   && ball.getPosition().magnitude() < 1.0/0.0)
				return false;
		}
		
		// no red ball found
		return true;
	}

	public boolean inFinalMode()
	{
		return finalMode;
	}
	
	private int[] isFirstTouchLegalFinalMode(Ball firstTouch, Ball[] previousPos)
	{
		// check in ascending order which of the colours was on the table before the shot. the lowest color is the one that's on
		
		// filter out the coloured balls (only the cue ball is left as non-coloured ball)
		Ball[] colours = new Ball[previousPos.length];
		int[] returnArray = new int[2];
		int j = 0;
		for(int i = 0; i < previousPos.length; i++) {
			
			if(previousPos[i].getValue() > 1) colours[j++] = previousPos[i];
		}
		Ball[] tmp = new Ball[j];
		System.arraycopy(colours, 0, tmp, 0, j);
		colours = tmp;
		
		// sort the balls in colours
		Arrays.sort(colours);
		
	    
		if (firstTouch == null) { /* No ball touched */
			returnArray[0] = -4;
			returnArray[1] = colours[0].getValue();
			return returnArray;
		}
		
		// loop through colours to find the lowest value ball and then check if it was the first touched
		for(int i = 0; i < colours.length; i++){
	
			if(colours[i] != null && colours[i].getPosition().magnitude() < 1.0/0.0) {
				
				returnArray[1] = colours[i].getValue();
			
				if(firstTouch.getValue() == colours[i].getValue()) {
					returnArray[0] = 0;
					
					return returnArray;
				}
				else {
				
					// the "on"-ball was not the first touch, penalty points are at least 4
					if(firstTouch.getValue() < 4) {
						returnArray[0] = -4;
						
						return returnArray;
					}
					else {
						returnArray[0] = (firstTouch.getValue() * -1);
						
						return returnArray;
					}
				}
			}
		}
		
		// this might need some exception handling? we only get here if previousPos has been empty in the first place, which is unlogically and should not happen
		return returnArray;
	}
	
	private int wasPottingLegalFinalMode(Ball[] potted, Ball[] currentPos, int valueOnBall)
	{
		// check if and how many balls were potted
		
		if(potted == null || potted.length == 0) return 0;
		
		else if(potted.length == 1) {
			
			// check if the potted ball was the on-ball
			if(potted[0].getValue() == valueOnBall) return potted[0].getValue();
			
			// it was not the on-ball
			else {
				
				if (potted[0].getValue() != 0)
					replace(potted[0], currentPos);
				
				// the "on"-ball was not the first touch, penalty points are at least 4
				if(potted[0].getValue() < 4) return -4;
				else return (potted[0].getValue() * -1);
			}
		}
		// more than 1 ball potted: foul
		else if(potted.length > 1) {
			
			// sort balls in potted and replace them
			Arrays.sort(potted);
			
			for(int i = potted.length - 1; i >= 0; i--) {
				if (potted[i].getValue() > 1)
					replace(potted[i], currentPos);
			}
			
			if(potted[potted.length - 1].getValue() < 4) return -4;
			else return (potted[potted.length - 1].getValue() * -1);
		}
		
		// we should not get here. this only happens if some of the inputs were empty
		return -100;		
	}
	
	private void updatePoints(boolean isPlayerAPlaying, int points)
	{
		if(points < 0) {
			
			// if points is negative, then the points are penalty points and are added to the opponents score
			if(isPlayerAPlaying) pointsPlayerB += (points * -1);
			else pointsPlayerA += (points * -1);
		}
		else {
			
			if(isPlayerAPlaying) pointsPlayerA += points;
			else pointsPlayerB += points;
		}
		
		pointsLastShot = points;
	}
	
	private void changePlayer(boolean isPlayerAPlaying)
	{
		if (!isFinalMode(table.getBalls()))
			colourOn = false;
		else
			colourOn = true;
	}

	/* This method is thread-safe with regard to itself */
	public GameLogic clone(Table newTable)
	{
		GameLogic logic = new GameLogic();

		logic.colourOn = this.colourOn;
		logic.snookerMode = this.snookerMode;
		logic.finalMode = this.finalMode;
		logic.pointsPlayerA = this.pointsPlayerA;
		logic.pointsPlayerB = this.pointsPlayerB;
		logic.isPlayerAPlaying = this.isPlayerAPlaying;
		logic.xPos = this.xPos;
		logic.firstTouchedBall = this.firstTouchedBall;
		logic.pottedBalls = this.pottedBalls;
		assert(pottedBalls == null);
		logic.table = newTable;
		assert(newTable.getGameLogic() == this);
		newTable.setGameLogic(null);
		logic.tableBeforeShot = newTable.clone();
		newTable.setGameLogic(this);
		logic.yPos = this.yPos;
		logic.pointsLastShot = this.pointsLastShot;
		logic.players = this.players;

		return logic;
	}

	public void gameEnd()
	{
		
	}


	public boolean isGameOver()
	{
			
		if (table.getBalls(false).length == 1)	
		{
			if (table.getBalls(false)[0].getValue() == 0)
			{
				return true;
			}
		
		}
		return false;
	}

	public boolean isBallFoul()
	{
		return false;
	}

	public void applyPoints()
	{
	}

	public void ballPocketed(Ball b)
	{
		Ball[] newPottedBalls;
		if (pottedBalls != null) {
			newPottedBalls = new Ball[pottedBalls.length + 1];
			System.arraycopy(pottedBalls, 0, newPottedBalls, 0,
			                 pottedBalls.length);
			pottedBalls = newPottedBalls;
		} else {
			pottedBalls = new Ball[1];
		}
		pottedBalls[pottedBalls.length - 1] = b;

	}

	public void ballCollision(Ball a, Object o)
	{
		if (!(o instanceof Ball))
			return;
		Ball b = (Ball) o;
		if (firstTouchedBall == null) {
			if (a.getValue() == 0)
				firstTouchedBall = b;
			else if (b.getValue() == 0)
				firstTouchedBall = a;
		}
	}

	public void shotBegin()
	{
		pointsLastShot = 0;
		pottedBalls = null;
		firstTouchedBall = null;
		tableBeforeShot = table.clone();
	}
	
	public void shotEnd(boolean doReplace)
	{
		Ball[] balls = table.getBalls();
		if (doReplace) {
			for (Ball ball : balls) {
				if (ball.getColor() == Color.WHITE)
				{
					if (ball.getPosition().getElements()[0] == 1.0/0.0) {
						if (isPlayerAPlaying)
							/* Inverter so the new player
							   can replace the ball
							 */
							players[1].replaceCueBall(table, ball);
						else {
							players[0].replaceCueBall(table, ball);
						}
					}
				}
			}
		}
		wasShotLegal(firstTouchedBall, pottedBalls,
		             tableBeforeShot.getBalls(false),
		             table.getBalls(false));
		if (isFinalMode(table.getBalls())) {
			if (pottedBalls == null || pottedBalls[0].getValue() != 1)
				finalMode = true;
		}
		pottedBalls = null;
		firstTouchedBall = null;
	}

	public void shotEnd()
	{
		System.out.println("shotEnd() reached");
		shotEnd(true);
	}

	public int getPlayerPoints(boolean forPlayerA)
	{
		return forPlayerA ? pointsPlayerA : pointsPlayerB;
	}

	public Player getActivePlayer()
	{
		return isPlayerAPlaying ? players[0] : players[1];
	}
        
	public boolean getColouredOn()
	{
		return colourOn;
	}
	public int getPointsLastShot()
	{
		return pointsLastShot;
	}
	
}
	
